Skip to content

fix(accessibility): patch prototype command for class-based Nightwatch commands#57

Closed
kamal-kaur04 wants to merge 3 commits into
nightwatchjs:mainfrom
kamal-kaur04:fix/accessibility-executescript-prototype-patch
Closed

fix(accessibility): patch prototype command for class-based Nightwatch commands#57
kamal-kaur04 wants to merge 3 commits into
nightwatchjs:mainfrom
kamal-kaur04:fix/accessibility-executescript-prototype-patch

Conversation

@kamal-kaur04

Copy link
Copy Markdown
Contributor

Description

performScan never fired on browser.execute('mobile:scroll', ...) — and on every other class-based command listed in commands.json — during App Accessibility sessions. No accessibility scan was captured for those interactions.

Root cause

commandWrapper() patched originalCommand.command directly. That only works for web-element commands, which export a plain object (module.exports.command = fn). But executeScript/executeAsyncScript (and the api/protocol, api/client-commands, and appium commands) export a class with command on the prototype:

class ExecuteScript extends ClientCommand { command(script, args, cb) { ... } }
module.exports = ExecuteScript;

So originalCommand.command read a non-existent static method (originalCommandFn was undefined) and the wrapper assigned a new static method that Nightwatch never calls — it invokes the prototype command. The patch silently no-op'd, no error thrown.

Fix

Detect whether command lives as an own property (object export) or on the prototype (class export) and patch the correct target — mirroring how the protocolAction branch already handles class-based commands.

const commandTarget = typeof originalCommand.command === 'function'
  ? originalCommand
  : (originalCommand.prototype && typeof originalCommand.prototype.command === 'function'
    ? originalCommand.prototype
    : null);

The pre-existing shouldPatchExecuteScript recursion guard (dead code until now, since the patch never attached) becomes effective and prevents the plugin's own browserstack_executor scan scripts from re-triggering a scan.

How tested

  • Real-device Android App Accessibility session (@nightwatch/browserstack + appium).
  • app.execute('mobile:scroll', ...) now triggers exactly one performScan; instrumented trace confirms PATCH cmd=executeScript target=prototype and FIRE wrapped cmd=executeScript arg0=mobile:scroll.
  • No recursion — the 11 internal browserstack_executor: {...appAllyScan...} re-entries all hit the guard and skipped re-scanning.
  • Test passes; eslint clean.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature
  • Breaking change

kamal-kaur04 and others added 3 commits June 10, 2026 18:03
…h commands

commandWrapper() patched `originalCommand.command` directly, which only works
for web-element commands that export a plain object (`module.exports.command = fn`).
executeScript/executeAsyncScript (and the protocol/client/appium commands) export
a class with `command` on the prototype, so the patch landed on a non-existent
static method and the real prototype command Nightwatch invokes stayed unpatched.

As a result `performScan` never ran for `browser.execute('mobile:scroll', ...)`
(and all other class-based commands listed in commands.json) on App Accessibility
sessions — no accessibility scan was captured for those interactions.

Detect whether `command` lives as an own property (object export) or on the
prototype (class export) and patch the correct target, mirroring how the
`protocolAction` branch already handles class-based commands. The existing
`shouldPatchExecuteScript` recursion guard now becomes effective and prevents the
plugin's own `browserstack_executor` scan scripts from re-triggering a scan.

Verified on a real-device Android App Accessibility session: `mobile:scroll` now
triggers exactly one performScan, with no recursion, and the test passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…r scripts

shouldPatchExecuteScript() treated any non-string script as a script to skip
(`!script || typeof script !== 'string'` -> return true), so user scripts passed
as functions — `browser.execute(function(){...})` / `executeAsyncScript(fn)` — never
triggered performScan, even though they can mutate page/screen state just like a
string script.

The plugin's own scan scripts are always strings carrying the `browserstack_executor`
token (verified: every internal execute* call in the SDK passes a string), so a
non-string script is always a user script and is safe to scan — the string-based
recursion guard is unaffected.

Treat an empty/undefined script as skip (unchanged) and a function script as a user
script to scan.

Verified on a real-device Android App Accessibility session: executeAsyncScript(fn)
now triggers exactly one performScan; the 18 internal browserstack_executor scan
scripts are still skipped (no recursion); test passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…eScript guard

Adds unit tests for the two accessibility command-wrapping fixes:

- shouldPatchExecuteScript: empty -> skip, internal browserstack_executor /
  accessibility scripts -> skip, user string scripts -> scan, and (regression)
  function-form user scripts -> scan.
- commandWrapper: wraps the own `command` of object-export (web-element) commands
  and the prototype `command` of class-export commands (executeScript) without
  creating a phantom static; verifies the wrapped command triggers performScan,
  delegates to the original, honours the recursion guard, and scans function-form
  scripts. Drives commandWrapper deterministically by stubbing
  require.resolve('nightwatch') and injecting fake command modules via require.cache.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@kamal-kaur04

Copy link
Copy Markdown
Contributor Author

Merged in #58

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants